/* * Copyright 2013 two forty four a.m. LLC <http://www.twofortyfouram.com> * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * <http://www.apache.org/licenses/LICENSE-2.0> * * Unless required by applicable law or agreed to in writing, software distributed under the License is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and limitations under the License. */ package it.angelic.soulissclient.test; import android.annotation.TargetApi; import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Build; import android.os.Bundle; import android.os.Looper; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; import android.text.TextUtils; import android.widget.EditText; import com.twofortyfouram.locale.BreadCrumber; import java.lang.reflect.Field; import it.angelic.bundle.PluginBundleManager; import it.angelic.soulissclient.R; import it.angelic.soulissclient.TaskerEditActivity; /** * Tests the {@link TaskerEditActivity}. */ public final class TaskerEditActivityTest extends ActivityInstrumentationTestCase2<TaskerEditActivity> { /** * Context of the target application. This is initialized in {@link #setUp()}. */ private Context mTargetContext; /** * Instrumentation for the test. This is initialized in {@link #setUp()}. */ private Instrumentation mInstrumentation; /** * Constructor for the test class; required by Android. */ public TaskerEditActivityTest() { super(TaskerEditActivity.class); } /** * Setup that executes before every test case */ @Override protected void setUp() throws Exception { super.setUp(); mInstrumentation = getInstrumentation(); mTargetContext = mInstrumentation.getTargetContext(); /* * Perform test case specific initialization. This is required to be set up here because * setActivityIntent has no effect inside a method annotated with @UiThreadTest */ if ("testNewSettingCancel".equals(getName())) //$NON-NLS-1$ { setActivityIntent(new Intent(com.twofortyfouram.locale.Intent.ACTION_EDIT_SETTING).putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation")); //$NON-NLS-1$ } else if ("testNewSettingSave".equals(getName())) //$NON-NLS-1$ { setActivityIntent(new Intent(com.twofortyfouram.locale.Intent.ACTION_EDIT_SETTING).putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation")); //$NON-NLS-1$ } else if ("testOldSetting".equals(getName())) //$NON-NLS-1$ { final Bundle bundle = PluginBundleManager.generateBundle(mTargetContext, "I am a toast message!"); //$NON-NLS-1$ setActivityIntent(new Intent(com.twofortyfouram.locale.Intent.ACTION_EDIT_SETTING).putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation").putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, bundle)); //$NON-NLS-1$ } else if ("testOldSetting_screen_rotation".equals(getName())) //$NON-NLS-1$ { final Bundle bundle = PluginBundleManager.generateBundle(mTargetContext, "I am a toast message!"); //$NON-NLS-1$ setActivityIntent(new Intent(com.twofortyfouram.locale.Intent.ACTION_EDIT_SETTING).putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation").putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, bundle)); //$NON-NLS-1$ } else if ("testMissingBreadcrumb".equals(getName())) //$NON-NLS-1$ { setActivityIntent(new Intent(com.twofortyfouram.locale.Intent.ACTION_EDIT_SETTING)); } else if ("testBadBundle".equals(getName())) //$NON-NLS-1$ { final Bundle bundle = PluginBundleManager.generateBundle(mTargetContext, "I am a toast message!"); //$NON-NLS-1$ bundle.putString(PluginBundleManager.BUNDLE_EXTRA_STRING_MESSAGE, null); setActivityIntent(new Intent(com.twofortyfouram.locale.Intent.ACTION_EDIT_SETTING).putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB, "Locale > Edit Situation").putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, bundle)); //$NON-NLS-1$ } } /** * Verifies the Activity class name hasn't been accidentally changed. */ @SmallTest public static void testActivityName() { /* * NOTE: This test is expected to fail initially when you are adapting this example to your own * plug-in. Once you've settled on a name for your Activity, go ahead and update this test case. * * The goal of this test case is to prevent accidental renaming of the Activity. Once a plug-in is * published to the app store, the Activity shouldn't be renamed because that will break the plug-in * for users who had the old version of the plug-in. If you ever find yourself really needing to * rename the Activity after the plug-in has been published, take a look at using an activity-alias * entry in the Android Manifest. */ assertEquals("it.angelic.soulissclient.TaskerEditActivity", TaskerEditActivity.class.getName()); //$NON-NLS-1$ } /** */ @MediumTest public void testGetBlurb() { assertEquals("Foo", TaskerEditActivity.generateBlurb(mTargetContext, "Foo")); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Tests creation of a new setting, that the UI is initialized to the right state, and that the Activity * result is correct if the user doesn't input anything. */ @MediumTest @UiThreadTest public void testNewSettingCancel() throws Throwable { final Activity activity = getActivity(); assertTitle(); assertMessageAutoSync(""); //$NON-NLS-1$ assertHintAutoSync(mTargetContext.getString(R.string.manual_cmd_hint)); activity.finish(); assertEquals(Activity.RESULT_CANCELED, getActivityResultCode(activity)); } /** * Tests editing a new condition with screen rotations. */ @MediumTest public void testNewSetting_screen_rotation() throws Throwable { /* * At this point, nothing is selected. Rotate the screen to make sure that this state is preserved. */ assertMessageAutoSync(""); //$NON-NLS-1$ setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); assertMessageAutoSync(""); //$NON-NLS-1$ /* * Select something and rotate the screen to make sure that this state is preserved. */ setMessageAutoSync("foo"); //$NON-NLS-1$ assertMessageAutoSync("foo"); //$NON-NLS-1$ setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); assertMessageAutoSync("foo"); //$NON-NLS-1$ /* * Select something and rotate the screen to make sure that this state is preserved. */ setMessageAutoSync("bar"); //$NON-NLS-1$ assertMessageAutoSync("bar"); //$NON-NLS-1$ setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); assertMessageAutoSync("bar"); //$NON-NLS-1$ } /** * Tests creation of a new setting, that the UI is initialized to the right state, and that changes are * properly saved */ @MediumTest @UiThreadTest public void testNewSettingSave() throws Throwable { final Activity activity = getActivity(); assertTitle(); assertMessageAutoSync(""); //$NON-NLS-1$ assertHintAutoSync(mTargetContext.getString(R.string.manual_cmd_hint)); setMessageAutoSync(getName()); activity.finish(); assertActivityResultAutoSync(getName()); } /** * Tests editing an old setting, that the UI is initialized to the right state, and that the Activity * result is correct. */ @MediumTest @UiThreadTest public void testOldSetting() throws Throwable { final Activity activity = getActivity(); assertTitle(); assertMessageAutoSync("I am a toast message!"); //$NON-NLS-1$ activity.finish(); assertActivityResultAutoSync("I am a toast message!"); //$NON-NLS-1$ } /** * Tests editing an old setting with screen rotations. */ @MediumTest public void testOldSetting_screen_rotation() throws Throwable { /* * Make sure that the initial state is preserved. */ assertMessageAutoSync("I am a toast message!"); //$NON-NLS-1$ setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); assertMessageAutoSync("I am a toast message!"); //$NON-NLS-1$ /* * Select something and rotate the screen to make sure that this state is preserved. */ setMessageAutoSync("foo"); //$NON-NLS-1$ assertMessageAutoSync("foo"); //$NON-NLS-1$ setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setActivityOrientationSync(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); assertMessageAutoSync("foo"); //$NON-NLS-1$ } /** * Verifies the Activity properly handles a missing breadcrumb. */ @MediumTest @UiThreadTest public void testMissingBreadcrumb() { /* * Not much to do here, the work was done in setUp. If the Activity fails to load, then this test case * will fail. */ assertTitle(); } /** * Verifies the Activity properly handles a bundle with a bad value embedded in it. */ @MediumTest @UiThreadTest public void testBadBundle() throws Throwable { final Activity activity = getActivity(); assertTitle(); assertMessageAutoSync(""); //$NON-NLS-1$ assertHintAutoSync(mTargetContext.getString(R.string.manual_cmd_hint)); activity.finish(); assertEquals(Activity.RESULT_CANCELED, getActivityResultCode(activity)); } /** * Asserts the Activity title equals expected values. */ private void assertTitle() { final CharSequence expected = BreadCrumber.generateBreadcrumb(mTargetContext, getActivity().getIntent(), mTargetContext.getString(R.string.souliss_app_name)); final CharSequence actual = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? getTitleHoneycomb() : getActivity().getTitle(); assertEquals(expected, actual); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private CharSequence getTitleHoneycomb() { return getActivity().getActionBar().getSubtitle(); } /** * Asserts the Activity result contains the expected values for the given display state. * * @param message The message the plug-in is supposed to show. */ private void assertActivityResultAutoSync(final String message) throws Throwable { final Activity activity = getActivity(); final Runnable runnable = new Runnable() { public void run() { /* * Verify finishing with text entry is saved */ activity.finish(); assertEquals(Activity.RESULT_OK, getActivityResultCode(activity)); final Intent result = getActivityResultData(activity); assertNotNull(result); final Bundle extras = result.getExtras(); assertNotNull(extras); assertEquals(String.format("Extras should only contain %s and %s but actually contain %s", com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB, extras.keySet()), 2, extras.keySet() //$NON-NLS-1$ .size()); assertFalse(TextUtils.isEmpty(extras.getString(com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB))); assertEquals(TaskerEditActivity.generateBlurb(mTargetContext, message), extras.getString(com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB)); // BundleTestHelper.assertSerializable(extras); final Bundle pluginBundle = extras.getBundle(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE); assertNotNull(pluginBundle); /* * Verify the Bundle can be serialized */ // BundleTestHelper.assertSerializable(pluginBundle); /* * The following are tests specific to this plug-in's bundle */ assertTrue(PluginBundleManager.isBundleValid(pluginBundle)); assertEquals(message, pluginBundle.getString(PluginBundleManager.BUNDLE_EXTRA_STRING_MESSAGE)); } }; if (getActivity().getMainLooper() == Looper.myLooper()) { runnable.run(); } else { runTestOnUiThread(runnable); } } /** * Asserts provided message is what the UI shows. * * @param message Message to assert equals the EditText. */ private void assertMessageAutoSync(final String message) throws Throwable { final Runnable runnable = new Runnable() { public void run() { assertEquals(message, ((EditText) getActivity().findViewById(android.R.id.text1)).getText() .toString()); } }; if (getActivity().getMainLooper() == Looper.myLooper()) { runnable.run(); } else { runTestOnUiThread(runnable); } } /** * Asserts provided hint is what the UI shows. */ private void assertHintAutoSync(final String hint) throws Throwable { final Runnable runnable = new Runnable() { public void run() { assertEquals(hint, ((EditText) getActivity().findViewById(android.R.id.text1)).getHint() .toString()); } }; if (getActivity().getMainLooper() == Looper.myLooper()) { runnable.run(); } else { runTestOnUiThread(runnable); } } /** * Sets the message. * * @param message The message to set. */ private void setMessageAutoSync(final String message) throws Throwable { final Runnable runnable = new Runnable() { public void run() { final EditText editText = (EditText) getActivity().findViewById(android.R.id.text1); editText.setText(message); } }; if (getActivity().getMainLooper() == Looper.myLooper()) { runnable.run(); } else { runTestOnUiThread(runnable); } } /** * Helper to get the Activity result code via reflection. * * @param activity Activity whose result code is to be obtained. * @return Result code of the Activity. */ private static int getActivityResultCode(final Activity activity) { if (null == activity) { throw new IllegalArgumentException("activity cannot be null"); //$NON-NLS-1$ } /* * This is a hack to verify the result code. There is no official way to check this using the Android * testing frameworks, so accessing the internals of the Activity object is the only way. This could * break on newer versions of Android. */ try { final Field resultCodeField = Activity.class.getDeclaredField("mResultCode"); //$NON-NLS-1$ resultCodeField.setAccessible(true); return ((Integer) resultCodeField.get(activity)).intValue(); } catch (final Exception e) { throw new RuntimeException(e); } } /** * Helper method to set the Activity's orientation. When this method completes, the Activity will have * finished its Activity lifecycle events. * <p/> * This method must not be called from the UI thread. */ private void setActivityOrientationSync(final int orientation) { mInstrumentation.runOnMainSync(new Runnable() { public void run() { getActivity().setRequestedOrientation(orientation); } }); } /** * Helper to get the Activity result Intent via reflection. * * @param activity Activity whose result Intent is to be obtained. Cannot be null. * @return Result Intent of the Activity * @throws IllegalArgumentException if {@code activity} is null */ private static Intent getActivityResultData(final Activity activity) { if (null == activity) { throw new IllegalArgumentException("activity cannot be null"); //$NON-NLS-1$ } /* * This is a hack to verify the result code. There is no official way to check this using the Android * testing frameworks, so accessing the internals of the Activity object is the only way. This could * break on newer versions of Android. */ try { final Field resultIntentField = Activity.class.getDeclaredField("mResultData"); //$NON-NLS-1$ resultIntentField.setAccessible(true); return ((Intent) resultIntentField.get(activity)); } catch (final Exception e) { throw new RuntimeException(e); } } }